Skip to content

Bonus: Input Cheatsheet

(Optional lesson)

There are lots of different form controls on the web, and it can be hard to remember exactly which properties each one takes.

This lesson is an appendix that details how to use the most common form inputs.

I suggest bookmarking this lesson. Whenever you're building a form, I hope this lesson can jog your memory!

Text inputs

For controlled text inputs, we bind the React state to the value attribute. We can set the initial value for uncontrolled text inputs with defaultValue.

Here's how to use a controlled text input:

Code Playground

import React from 'react';

function App() {
const [name, setName] = React.useState('');
return (
<>
<form
onSubmit={(event) => {
event.preventDefault();
// Do something with `name` here
}}
>
<label htmlFor="name-field">
Name:
</label>
<input
id="name-field"
value={name}
onChange={event => {
setName(event.target.value);
}}
/>
</form>
<p>
<strong>Current value:</strong>
{name || '(empty)'}
</p>
</>
);
}

export default App;

Gotchas

When working with controlled text inputs, be sure to use an empty string ('') as the initial state. Otherwise, you risk running into edge-cases caused by the flip from uncontrolled to controlled.

// 🚫 Incorrect:
const [email, setEmail] = React.useState();
// ✅ Correct:
const [email, setEmail] = React.useState('');

For more information about why this is necessary, check out the “Data Binding” lesson.

Text input variants

In addition to plain text inputs, we can pick from different “formatted” text inputs, for things like email addresses, phone numbers, and passwords.

Here's the good news: These variants all work the same way, as far as data binding is concerned.

For example, here's how we'd bind a password input:

const [secret, setSecret] = React.useState('');
<input
type="password"
value={secret}
onChange={(event) => {
setSecret(event.target.value);
}}
/>

In addition to text input variants, the <input> tag can also shape-shift into entirely separate form controls. Later in this lesson, we'll talk about radio buttons, checkboxes, and specialty inputs like sliders and color pickers.

Textareas

In React, <textarea> elements work exactly like text inputs. We set value to bind it to React state, and defaultValue to set an initial value for uncontrolled components.

Here's how to use a controlled textarea:

Code Playground

import React from 'react';

function App() {
const [comment, setComment] = React.useState('');
return (
<>
<form
onSubmit={(event) => {
event.preventDefault();
}}
>
<label htmlFor="comment-field">
Share your experiences:
</label>
<textarea
id="comment-field"
value={comment}
onChange={event => {
setComment(
event.target.value
);
}}
/>
</form>
<p>
<strong>Current value:</strong>
{comment || '(empty)'}
</p>
</>
);
}

export default App;

Gotchas

As with text inputs, be sure to use an empty string ('') as the initial state. Otherwise, you risk running into edge-cases caused by the flip from uncontrolled to controlled.

// 🚫 Incorrect:
const [email, setEmail] = React.useState();
// ✅ Correct:
const [email, setEmail] = React.useState('');

Radio buttons

Things are a bit different when it comes to radio buttons. To wire up a radio button so that it's controlled by React, we need to set the checked property to a boolean value. It specifies whether the radio button is currently ticked or not.

The defaultChecked property can be used to set the initial value without making it a controlled input.

Minimal controlled example

Code Playground

import React from 'react';

function App() {
const [hasAgreed, setHasAgreed] = React.useState();

return (
<>
<form
onSubmit={(event) => {
event.preventDefault();
}}
>
<fieldset>
<legend>
Do you agree?
</legend>
<input
type="radio"
name="agreed-to-terms"
id="agree-yes"
value="yes"
checked={hasAgreed === "yes"}
onChange={event => {
setHasAgreed(event.target.value)
}}
/>
<label htmlFor="agree-yes">
Yes
</label>
<br />
<input
type="radio"
name="agreed-to-terms"
id="agree-no"
value="no"
checked={hasAgreed === "no"}
onChange={event => {
setHasAgreed(event.target.value)
}}
/>
<label htmlFor="agree-no">
No
</label>
</fieldset>
</form>
<p>
<strong>Has agreed:</strong>
{hasAgreed || "undefined"}
</p>
</>
);
}

export default App;

When it comes to radio buttons, there are lots of attributes to keep in mind. Here's a table summarizing them:

AttributeTypeExplanation
idstringA globally-unique identifier for this radio button, used to improve accessibility and usability.
namestringGroups a set of radio buttons together, so that only one can be selected at a time. Must be the same value for all radio buttons in the group.
valuestringSpecifies the “thing” that this radio button represents. This is what will be captured/stored if this particular option is selected.
checkedbooleanControls whether the radio button is checked or not. By passing a boolean value, React will make this a “controlled” input.
onChangefunctionLike other form controls, this function will be invoked when the user changes the selected option. We use this function to update our state.

Iterative controlled example

Because radio buttons have so many dang attributes, it often helps to generate them iteratively; that way, we only have to write the JSX once!

This is also required when the options themselves are dynamic (eg. fetched from the server).

Here's an example:

Code Playground

import React from 'react';

function App() {
const [
language,
setLanguage
] = React.useState('english');

return (
<>
<form
onSubmit={(event) => {
event.preventDefault();
}}
>
<fieldset>
<legend>
Select language:
</legend>
{VALID_LANGUAGES.map(option => (
<div key={option}>
<input
type="radio"
name="current-language"
id={option}
value={option}
checked={option === language}
onChange={event => {
setLanguage(event.target.value);
}}
/>
<label htmlFor={option}>
{option}
</label>
</div>
))}
</fieldset>
</form>
<p>
<strong>Selected language:</strong>
{language || "undefined"}
</p>
</>
);
}

const VALID_LANGUAGES = [
'mandarin',
'spanish',
'english',
'hindi',
'arabic',
'portugese',
];

export default App;

Gotchas

When using iteration to dynamically create radio buttons, we need to be careful not to accidentally “re-use” a variable name used by our state variable.

Avoid doing this:

const [language, setLanguage] = React.useState();
return VALID_LANGUAGES.map((language) => (
<input
type="radio"
name="current-language"
id={language}
value={language}
checked={language === language}
onChange={event => {
setLanguage(event.target.value);
}}
/>
));

In our .map() call, we're naming the map parameter language, but that name is already taken! Our state variable is also called language.

This is known as “shadowing”, and it essentially means that we've lost access to the outer language value. This is a problem, because we need it to accurately set the checked attribute!

For this reason, I like to use the generic option name when iterating over possible options:

VALID_LANGUAGES.map(option => {
<input
type="radio"
name="current-language"
id={option}
value={option}
checked={option === language}
onChange={event => {
setLanguage(event.target.value);
}}
/>
})

Checkboxes

As with radio buttons, the checked property is used to create a controlled element. It should be a boolean value, specifying whether the checkbox is currently ticked or not.

The defaultChecked property can be used to set the initial value without making it a controlled input.

With checkboxes, the approach differs depending on whether we're working with a single checkbox or a group of checkboxes. Let's look at each in turn.

Single checkbox example

Code Playground

import React from 'react';

function App() {
const [optIn, setOptIn] = React.useState(false);

return (
<>
<form
onSubmit={(event) => {
event.preventDefault();
}}
>
<input
type="checkbox"
id="opt-in-checkbox"
checked={optIn}
onChange={event => {
setOptIn(event.target.checked);
}}
/>
<label htmlFor="opt-in-checkbox">
<strong>Yes,</strong> I would like to join the newsletter.
</label>
</form>
<p>
<strong>Opt in:</strong> {optIn.toString()}
</p>
</>
);
}

export default App;

We store a boolean state variable, optIn, and set it as the checked value. When optIn is true, the checkbox is ticked. Otherwise, the checkbox is unticked.

Things get trickier when we need to drive multiple checkboxes.

Multiple checkbox example

There are several ways to do this, but my favourite is to use a map-like object. Here's an example:

Code Playground

import React from 'react';

const initialToppings = {
anchovies: false,
chicken: false,
tomatoes: false,
}

function App() {
const [
pizzaToppings,
setPizzaToppings
] = React.useState(initialToppings);

// Get a list of all toppings.
// ['anchovies', 'chicken', 'tomato'];
const toppingsList = Object.keys(initialToppings);
return (
<>
<form
onSubmit={(event) => {
event.preventDefault();
}}
>
<fieldset>
<legend>
Select toppings:
</legend>
{/*
Iterate over those toppings, and
create a checkbox for each one:
*/}
{toppingsList.map(option => (
<div key={option}>
<input
type="checkbox"
id={option}
value={option}
checked={pizzaToppings[option] === true}
onChange={event => {
setPizzaToppings({
...pizzaToppings,
[option]: event.target.checked,
})
}}
/>
<label htmlFor={option}>
{option}
</label>
</div>
))}
</fieldset>
</form>
<p>
<strong>Stored state:</strong>
</p>
<p className="output">
{JSON.stringify(pizzaToppings, null, 2)}
</p>
</>
);
}

export default App;

With radio buttons, we can fit everything we need to know into a single string: the value of the selected option. But when we have a group of checkboxes, we need to store more data, since the user can select multiple options.

Here is how I choose to represent this state:

const initialToppings = {
anchovies: false,
chicken: false,
tomatoes: false,
}

In the JSX, we map over the keys from this object, and render a checkbox for each one. In the iteration, we look up whether this particular option is selected, and use it to control the checkbox with the checked attribute.

We also pass a function to onChange that will flip the value of the checkbox in question. Because React state needs to be immutable, we solve this by creating a near-identical new object, with the option in question flipped between true/false. I'm doing this with the help of the “spread” syntax 👀.

Here's a table showing each attribute's purpose:

AttributeTypeExplanation
idstringA globally-unique identifier for this checkbox, used to improve accessibility and usability.
valuestringSpecifies the “thing” that we're ticking off and on with this checkbox.
checkedbooleanControls whether the checkbox is checked or not.
onChangefunctionLike other form controls, this function will be invoked when the user ticks or unticks the checkbox. We use this function to update our state.

(We can also specify a name, as with radio buttons, though this isn't strictly necessary when working with controlled inputs.)

It may help to revisit the lessons on complex state.

Select

To create a controlled select tag, we use the value attribute. We update the value with an onChange handler. In effect, it works exactly like a text input!

For uncontrolled select tags, the initial value can be set with defaultValue.

Here's how to use a controlled select:

Code Playground

import React from 'react';

function App() {
const [age, setAge] = React.useState('0-18');

return (
<>
<form
onSubmit={(event) => {
event.preventDefault();
}}
>
<label htmlFor="age-select">
How old are you?
</label>
<select
id="age-select"
value={age}
onChange={event => {
setAge(event.target.value)
}}
>
<option value="0-18">
18 and under
</option>
<option value="19-39">
19 to 39
</option>
<option value="40-64">
40 to 64
</option>
<option value="65-infinity">
65 and over
</option>
</select>
</form>
<p>
<strong>Selected value:</strong>
{age}
</p>
</>
);
}

export default App;

Perhaps more than any other tag, <select> has been modified for React. In a vanilla HTML/JS context, you'd need to reach down and toggle the selected attribute on the appropriate <option> child. Fortunately, this is not required when working with controlled select tags in React.

Gotchas

As with text inputs, we need to initialize the state to a valid value. This means that our state variable's initial value must match one of the options:

// This initial value:
const [age, setAge] = React.useState("0-18");
// Must match one of the options:
<select>
<option
value="0-18"
>
18 and under
</option>
</select>

This is a smelly fish. One small typo, and we risk running into some very confusing bugs.

To avoid this potential footgun, I prefer to generate the <option> tags dynamically, using a single source of truth:

Code Playground

import React from 'react';

// The source of truth!
const OPTIONS = [
{
label: '18 and under',
value: '0-18'
},
{
label: '19 to 39',
value: '19-39'
},
{
label: '40 to 64',
value: '40-64'
},
{
label: '65 and over',
value: '65-infinity'
},
];

function App() {
// Grab the first option from the array.
// Set its value into state:
const [age, setAge] = React.useState(OPTIONS[0].value);

return (
<>
<form
onSubmit={(event) => {
event.preventDefault();
}}
>
<label htmlFor="age-select">
How old are you?
</label>
<select
id="age-select"
value={age}
onChange={event => {
setAge(event.target.value)
}}
>
{/*
Iterate over that array, to create
the <option> tags dynamically:
*/}
{OPTIONS.map(option => (
<option
key={option.value}
value={option.value}
>
{option.label}
</option>
))}
</select>
</form>
<p>
<strong>Selected value:</strong>
{age}
</p>
</>
);
}

export default App;

Specialty inputs

As we've seen, the <input> HTML tag can take many different forms. Depending on the type attribute, it can be a text input, a password input, a checkbox, a radio button…

In fact, MDN lists 22 different valid values for the type attribute. Some of these are “special”, and have a unique appearance:

  • Sliders (with type="range")
  • Date pickers (with type="date")
  • Color pickers (with type="color")

Fortunately, they all follow the same pattern as text inputs. We use value to lock the input to the state's value, and onChange to update that value when the input is edited.

Here's an example using <input type="range">:

Code Playground

import React from 'react';

function App() {
const [volume, setVolume] = React.useState(50);
return (
<>
<form
onSubmit={(event) => {
event.preventDefault();
}}
>
<label htmlFor="volume-slider">
Audio volume:
</label>
<input
type="range"
id="volume-slider"
min={0}
max={100}
value={volume}
onChange={event => {
setVolume(event.target.value);
}}
/>
</form>
<p>
<strong>Current value:</strong>
{volume}
</p>
</>
);
}

export default App;

Here's another example, with <input type="color">:

Code Playground

import React from 'react';

function App() {
const [color, setColor] = React.useState('#FF0000');
return (
<>
<form
onSubmit={(event) => {
event.preventDefault();
}}
>
<label htmlFor="color-picker">
Select a color:
</label>
<input
type="color"
id="color-picker"
value={color}
onChange={event => {
setColor(event.target.value);
}}
/>
</form>
<p>
<strong>Current value:</strong>
{color}
</p>
</>
);
}

export default App;

Gotchas

As with text inputs, we don't want to flip from uncontrolled to controlled. We need to initialize our state variable to a valid value.

For example, for range inputs, this would be a number:

// 🚫 Incorrect:
const [value, setValue] = React.useState();
// ✅ Correct:
const [value, setValue] = React.useState(5);